feat(setup-aionui): AionUI 集成 — 一键安装 Track Changes 技能 + 注册 Word 修订助手#131
Open
NextDoorLaoHuang-HF wants to merge 2746 commits into
Open
feat(setup-aionui): AionUI 集成 — 一键安装 Track Changes 技能 + 注册 Word 修订助手#131NextDoorLaoHuang-HF wants to merge 2746 commits into
NextDoorLaoHuang-HF wants to merge 2746 commits into
Conversation
… tokens The morph helper recognized these tokens as input aliases for 'byChar', but the upstream knownDirTokens gate rejected them with 'Invalid transition modifier: bychar in morph-bychar'. The gate's allowlist had 'byword' / 'byobject' / 'byletter' but no 'bychar' entry. Add 'bychar', 'char', 'character' to the gate (matching the morph helper's accepted aliases) and round out the existing 'byword' / 'byobject' with their bare forms 'word' / 'object' which the helper already accepts. Net effect: transition=morph-bychar now flows through the gate, into the morph helper, and writes <p159:morph option="byChar"/> as expected.
…a tokens
Adds an examples/ppt/transitions/ subdirectory mirroring the existing
charts/ and tables/ layout. Eight trios (each: .sh build script, .md
walkthrough, .pptx generated deck) enumerate every transition token
declared in schemas/help/pptx/transition.json, grouped by mechanism
rather than by name:
- transitions-basic — cut / fade / dissolve / flash + 'none' clear
- transitions-directional — push, wipe (4 dirs); cover, uncover (8 dirs)
- transitions-shapes — circle / diamond / plus / wedge + zoom in/out
+ wheel-N spoke count (1..8)
- transitions-bands — blinds / checker / comb / bars (vert/horiz),
strips (4 corners), split (orient × in/out),
+ venetian/checkerboard/randombar/diagonal aliases
- transitions-dynamic — 2010+ Exciting gallery: switch/flip/ferris/
gallery/conveyor/reveal/shred/flythrough/
warp/vortex/glitter/pan/prism/doors/window/
ripple/honeycomb
- transitions-random — newsflash, random
- transitions-timing — speed/duration/advanceTime/advanceClick knobs
- transitions-morph — 2016+ Morph (byobject/byword/bychar)
Each .md notes the canonical readback form (e.g. 'pull' input → 'uncover'
readback), call out which families accept which direction modifiers
(push: 4 cardinal; cover: 8; etc.), and document one known officecli
limitation (transition=box writes invalid <p:box> XML — separate parser
bug, the trio avoids it).
examples/README.md tree and quick-start section updated to surface the
new directory.
… element `set / gapwidth=N` on a combo chart silently lost the value: the RebuildComboChart path (deferred via the `comboTypes=` prop) replaces the original barChart with freshly constructed barChart elements that have no GapWidth child, but the gapwidth setter only walked existing `Descendants<GapWidth>()` and never seeded one. Mirror the `overlap` upsert pattern — insert a `<c:gapWidth>` before the first AxisId of each bar/bar3D child when none exist, preserving the schema order [barDirection, barGrouping, varyColors, ser*, gapWidth, overlap?, axisId+] that BuildBarChart seeds. Exposed by dump→replay round-trip on combo bar/line charts where the source gapWidth disappeared from the second pass.
…dHeaders
`dump /` on a doc whose settings.xml carries <w:evenAndOddHeaders/> and
that has an even header emitted `noEvenAndOddHeaders=true` on the
`add header type=even` row. Replay then suppressed the auto-stamp and
the round-tripped doc lost the toggle.
Root cause: the emitter called `word.Get("/settings")` and read
`Format["evenAndOddHeaders"]` to detect the source toggle, but
PopulateDocSettings only runs for the root node — Get("/settings")
returns a node with an empty Format dict, so the check always saw
"toggle missing" and unconditionally emitted the opt-out.
Read from `word.Get("/")` instead, whose Format IS populated. Mirrors
the sibling `titlePage` check above, which reads off the section node
(also populated by its own helper).
…rors `officecli dump corrupted.pptx /` and the docx equivalent leaked raw System.IO.FileFormatException / OpenXmlPackageException out of the handler constructor. SafeRun caught it at the CLI surface, but programmatic callers (tests, resident batch step) saw the unwrapped SDK exception with no `Code` or actionable suggestion. Route the open through DocumentHandlerFactory.Open, which already wraps both exception types into CliException(code=corrupt_file) with a "Verify the file is a valid .docx/.xlsx/.pptx" suggestion — the same contract every other top-level command (get, set, query, check, raw) follows. Cast the returned IDocumentHandler to the format-specific handler so the emitter call sites stay unchanged.
PowerPoint's "Box" transition has no <p:box> element in the basic OOXML
namespace — the <p:transition> child whitelist runs circle/diamond/.../
zoom and stops there. Earlier `transition=box` emitted <p:box dir="in"/>,
which made `officecli validate` reject the file and PowerPoint silently
ignore the transition.
PowerPoint 2013+ actually stores Box as <p15:prstTrans prst="box"/>
inside an mc:AlternateContent wrapper, with the inside-vs-outside
variants encoded as invX/invY booleans. Route box through the same
mc:AlternateContent path morph and p14 transitions already use, with
the p15 namespace and an inline <p:fade/> fallback. `box-in` is the
default (no invs), `box-out` flips both, anything else is rejected.
Also fix a readback hole the move uncovered: in PublishTrimmed Release
builds, the SDK's typed ChildElements collection on a wrapped <p:transition>
can drop unknown elements like <p15:prstTrans>, leaving typed access
non-null but with an empty child list. ReadSlideTransition now treats
that case as "typed access failed" and falls back to the OuterXml regex
path, matching how morph/p14 already round-trip.
Delete the dead BuildBoxTransition helper that emitted the invalid
<p:box>. Add tests pinning box-in/box-out/box-up and the trimmed-build
regression in PptxTransitionR24Tests.cs.
examples/ppt/transitions/transitions-shapes.{sh,md,pptx} put box back
into the shapes trio (it had been temporarily removed while the bug was
open) and document the p15 storage detail.
…ions Adds fallOver, drape, curtains, wind, prestige, fracture, crush, peelOff, pageCurlDouble, pageCurlSingle, airplane, origami — the full PowerPoint 2013 "Exciting" / "Dynamic Content" gallery. All twelve share one OOXML element with the previously-routed box transition: <p15:prstTrans prst="..."/> inside mc:AlternateContent, with CT_PresetTransition's invX / invY booleans encoding the -in (default, no invs) / -out (both invs set) direction modifier. Refactor: replace the box-specific code path with a small static _p15PrstTokens dictionary (key = case-insensitive CLI token, value = OOXML lowerCamelCase prst attribute) and a single TryGetValue-driven branch before the typed switch. Removes special-case intercepts and lets readback canonicalize directly from the prst attribute — the new presets get -out-suffix surfacing on readback for free. CLI token spelling matches the OOXML prst attribute (lowerCamelCase). Input is case-insensitive but Get returns the canonical spelling, so `transition=PAGECURLDOUBLE` round-trips as `pageCurlDouble`. The schema's prst attribute is declared as xsd:string (no enum), so the SDK has no validator to lean on — the 12-token whitelist comes from [MS-PPTX] CT_PresetTransition. Verified against the OPEN-XML-SDK local checkout: only one prst literal appears in test fixtures (pageCurlDouble), and the schema_microsoft_com_office_powerpoint_2012_main generated code confirms prst is freeform StringValidator(IsToken=true). schemas/help/pptx/transition.json: extend the enum list with the 12 new tokens (the schema is documentation, not enforcement; the help surface now matches what the parser accepts). Default-case error message in the typed switch extended to enumerate the new tokens so a typo produces a list that includes them. Test coverage (PptxTransitionR24Tests): 15 new round-trip cases (12 base + 3 -out) plus 3 case-insensitive input cases. Existing 41 tests still pass.
Ninth transition trio enumerating PowerPoint 2013+ "Exciting" gallery: fallOver, drape, curtains, wind, prestige, fracture, crush, peelOff, pageCurlDouble, pageCurlSingle, airplane, origami. 19-slide deck shows each bare form plus -out variants on the 6 direction-sensitive presets (symmetric ones parse the suffix but render unchanged — noted in the .md). examples/README.md tree and quick-start updated; cross-references in transitions-shapes.md / transitions-dynamic.md / transitions-morph.md already point at it via the "See also" sections.
…arning EmitWord stamps noMarkRPrInherit=true on every emitted `add r` row so the markRPr->rPr inheritance fill stays opt-out on dump->batch replay (matches the source's "run has no rFonts even though para mark does" shape). AddRun consumed the flag at line ~1587 to gate the inheritance fill, but the bare-key fallback at the bottom of AddRun never saw it on its curated allowlist. ApplyRunFormatting and TryCreateTypedChild both miss (it is not a real OOXML attribute), so every dump-emitted run landed the key in LastAddUnsupportedProps and the batch driver printed: WARNING: UNSUPPORTED props: noMarkRPrInherit Add "nomarkrprinherit" to addRunCuratedBare so the inheritance-toggle sentinel is treated as already-consumed instead of re-flagged as unknown.
…clock
Two findings from Mac PowerPoint round-trip on the modern gallery:
1. p15 -out variant: Mac PowerPoint's Effect Options writes a single
invX="1" attribute, not invX="1" invY="1" together. Setting both
makes PowerPoint silently reject the whole <p15:prstTrans> element
and play the mc:Fallback fade instead — the file opens with no
transition highlighted in the gallery and no preview animation.
Drop invY from the -out emission and the readback regex. Now matches
PowerPoint's own output byte-for-byte for direction-sensitive
presets (peelOff, airplane, origami, wind, fallOver, drape).
2. The "Cube" / "Rotate" / "Orbit" UI tiles are NOT separate elements —
they're all the same <p14:prism/> element with two boolean attrs:
bare <p14:prism/> → "Cube" (Exciting)
<p14:prism isContent="1"/> → "Rotate" (Dynamic Content)
<p14:prism isContent="1" isInverted="1"/> → "Orbit" (Dynamic Content)
"Clock" is similarly <p:wheel spokes="1"/> — a single-spoke wheel.
Wire all four as CLI tokens:
- `cube` accepted as input alias for `prism` (same bare XML, readback
stays as `prism` to preserve the OOXML element name)
- `rotate` and `orbit` are new canonical tokens, round-trip via
PrismTransition.IsContent / IsInverted
- `clock` accepted as input alias for `wheel-1` (readback stays as
`wheel-1`)
- Readback for <p14:prism> distinguishes the 3 variants by attr
combination so set transition=rotate/orbit round-trips
schemas/help/pptx/transition.json: extend enum with the 4 new tokens.
examples/ppt/transitions/transitions-dynamic.{sh,pptx}: replace the
broken prism-up / prism-right entries (prism is direction-less; the
suffix was silently dropped) with explicit prism / rotate / orbit
slides that actually round-trip.
examples/ppt/transitions/transitions-modern.{md,pptx}: regenerate with
the invX-only fix; add a "UI tiles backed by other elements" table
explaining the cube/rotate/orbit/clock cross-namespace mapping so
agents reading the modern trio know where to find those four tiles.
Tests (PptxTransitionR24Tests): 5 new round-trip cases (cube/prism/
rotate/orbit/clock). 64 transition tests pass.
…xplicit font EmitSection injected `set / docDefaults.font.latin=""` whenever the post- baseline-filter prop bag lacked a docDefaults.font key — intended to clear the BlankDocCreator-stamped Times New Roman when the source omits the slot. The baseline-skip loop directly above suppresses emits for keys whose source value matches the blank's value. When the source DOES carry an explicit `docDefaults.font` matching the blank (Calibri vs. Calibri) the skip fires and the prop is absent post-filter, which the empty-clear injection then misread as "source has no font" and stamped an empty string into the batch. Replay set the empty value, clearing the source font (rFonts ascii/hAnsi became blank) and dropping the document's intended typeface on round-trip. Guard the injection on the raw source Format presence instead of the filtered prop bag — only inject the empty clear when the source truly carries no docDefaults.font[.latin]. Existing TNR-clear path for source documents lacking the slot is preserved.
pptx dump emits `name` and `lang` on textbox add items (AddShape consumes
both — name defaults to "TextBox {N}" via cNvPr, lang stamps drawingML
rPr/@lang on the first run). pptx/textbox.json declared neither, so the
emitted batch tripped schema drift checks even though the handler accepted
the values.
textbox does not extend `_shared/shape`, so the `name` declaration in the
shared base does not flow through. Add `name` and `lang` directly on
pptx/textbox.json mirroring the pptx/shape.json declarations.
pptx dump emits zorder on shape/textbox add items: AddShape (Add.Shape.cs line 737) and ApplyShapePropsCore (Set.Shape.cs line 743) both consume zorder/z-order/order and reposition the shape in the slide shape tree. Schema declared zorder as get-only on both pptx/shape.json and pptx/textbox.json, so the emitted batch tripped schema-drift checks. Update pptx/shape.json to declare zorder add=true / set=true with the aliases (z-order, order) and an example. The matching textbox.json declaration was already updated in the previous commit when name/lang were added.
…files The .md walkthroughs are user-facing examples documentation focused on CLI usage; the OOXML element names, namespace prefixes, mc:Choice/ mc:Fallback wrapper anatomy, and prst attribute mechanics that crept in during development belong in source-code comments, not here. Rewrite the affected passages to describe the same behavior in CLI terms: - "OOXML representation" code blocks (modern, morph) — removed. - "Stored as <p:transition spd=...>" — replaced with "Get surfaces the value as the read-only transitionSpeed format key". - "<p14:reveal/> element had no dir attribute" / "p15 namespace" / "CT_OptionalBlackTransition" — replaced with plain-language descriptions of the user-visible behavior. - transitions-modern.md's "UI tiles backed by other elements" table drops the OOXML column, keeps just (UI name → CLI token). Net effect: the same agent or user reading these .md files now learns which CLI token to use and what to expect from Get/Preview, without the implementation-detail noise.
`get /slide[N]/shape[K]` bubbled the first run's size and color up to
the shape-level Format dictionary unconditionally. When the textbox
held runs with mixed formatting (e.g. run[1] red 28pt, run[2] blue
14pt), the shape-level Format reported only run[1]'s values, so an
agent inspecting `Format["color"]` couldn't tell whether the textbox
was uniformly red or actually mixed.
Walk every run in the text body once before emitting; if the size or
color set has more than one distinct value, suppress that key from
shape-level Format. `ContainsKey("size")` / `ContainsKey("color")` is
now the contract for "this is a meaningful summary".
Run-level Get is unchanged — `/slide[N]/shape[K]/paragraph[P]/run[R]`
still returns the per-run value. Set on the shape path also stays as
a broadcast (writes the same value to every run) so the asymmetry
between input and output stays explicit.
Heuristic stays narrow: only `size` and `color` are checked. Font /
bold / italic / underline etc. continue to surface the first run's
value — they have lower mixed-formatting incidence in practice and
expanding the probe risks suppressing keys users expect to see.
`add /slide[N] --type slide --prop title=T --prop text=B` emitted two shapes asymmetrically: the title got `<p:ph type="title"/>`, but the content was a bare textbox with no placeholder reference. On Get one came back as `type=title phType=title isTitle=true`, the other as a plain `type=textbox` with no phType. The content side also never inherited layout styling because it wasn't bound to any layout slot. Both shapes are auto-emitted in response to the same `add slide` shortcut and bind the same way conceptually — populate a layout slot with caller text. Tag the content shape with the matching marker so the two paths produce symmetric on-disk shapes and symmetric readback. CreateTextShape gains `placeholderType` / `placeholderIndex` optional parameters; the title path keeps its isTitle flag (with the implicit title placeholder), and the content path now passes `placeholderType: PlaceholderValues.Body, placeholderIndex: 1`. NodeBuilder's existing placeholder classifier handles the Get-side readback — no further changes there. Existing callers that didn't need a placeholder (everything except Add.Slide's content path) keep the previous default-null behavior. Side effect: `/slide[N]/placeholder[K]` now finds the content shape alongside the title, so direct slot addressing works without needing the `/slide[N]/shape[K]` form.
…d depth Two doc-only schema clarifications. pptx/slide.json — layout slot population Reading `help slide --json` showed `layout` documented as a metadata field with no guidance on what happens to the layout's placeholder slots. The actual contract: `layout` is metadata only — slots are NOT auto-materialized. Population paths are the `--prop title=…` / `--prop text=…` shortcuts (which emit shapes with <p:ph type="title"/> / <p:ph type="body" idx="1"/> markers respectively) and the explicit `--type placeholder --prop phType=…` route. Surface those three paths in the layout description, and cross-reference the OOXML markers in the `title` / `text` property descriptions so on-disk shape and Get readback (type=title vs type=placeholder phType=body) are predictable. pptx/table.json — default read depth `get /slide[N]/table[K]` returned table-level Format plus row stubs but empty cells, while `get /slide[N]/shape[K]` returned paragraphs and runs in one call. Reading the schema didn't reveal that `--depth N` already controls how deep Get descends. Document the knob in the table's top-level note: depth=1 is the row-stub summary; depth=2 materializes cells; direct addressing via `get .../row[R]/cell[C]` is always available. The default stays at 1 because large tables (e.g. 50x10) at depth=2 dominate the response.
`officecli add file.pptx --type slide` (no parent argument) raised the default System.CommandLine error "Required argument missing: <parent>" with no hint that pptx slide takes '/', docx takes /body, xlsx takes /Sheet1. New users had to guess the per-handler convention or read source. Expand the parent argument's Description so the per-handler examples appear in `officecli add --help` output. Also flag the zsh single- quote habit for paths with brackets, since unquoted /slide[1] gets glob-expanded by zsh and bash.
zsh treats `[N]` as a glob character class; bash with extglob also expands unquoted brackets. Copy-pasting an example like `--prop from=/slide[1]/shape[1]` from `officecli help` output into a zsh shell yielded `zsh: no matches found`, and the user had to recognize the brackets needed quoting. Wrap the path token (not the whole example string) in single quotes across the four `examples` arrays where bracketed paths appeared: schemas/help/pptx/connector.json — from / to shape refs schemas/help/pptx/moderncomment.json — parent comment ref schemas/help/pptx/picture.json — link=slide[N] action token schemas/help/xlsx/slicer.json — pivotTable ref `note` / `path` / `positional` fields (which document the canonical form, not copy-paste material) stay unquoted — they're definitions, not shell-ready strings.
Bug: EmitChart never emitted a `set /slide[N]/chart[K]/axis[@ROLE=ROLE]` row. Chart-level shortcuts (axisMin/axisMax/axisTitle) only target the primary value axis, so any per-role override — especially role=value2 on a secondary axis — was silently lost on dump→batch replay. role=value2 min/max/title all dropped; primary axis tick/format tweaks not covered by chart-level keys also dropped. Root cause: EmitChart only emitted a single `add chart` row with chart-level props. The Set side accepts axis sub-paths, but the dump side never walked them. Fix: After the add row, iterate {category, value, value2, series}. For each role, Get the axis sub-path, filter out BuildAxisNode's synthetic defaults (visible=true, majorGridlines true/false matching AddChart's seeded state, majorTickMark=out, crosses=autoZero, etc.), and emit a set row when any non-default key remains. Missing roles (pie / doughnut have no axes) are silently skipped via the Get-throws catch. New behavior: dump now round-trips per-axis min/max/title/tick/gridline overrides. Secondary value axis (role=value2) finally survives a replay. Chart types without the axis silently skip — no spurious rows for pie etc.
Bug: dump only emitted slideWidth / slideHeight on the `set /` row. firstSlideNum, rtl, show.loop, show.narration, show.useTimings, print.what, print.colorMode, compatMode, removePersonalInfo were all silently dropped on dump→batch replay. Root cause: EmitPresentationProps hard-coded the slide-size pair and ignored every other key TrySetPresentationSetting (Set.Presentation.cs) accepts. Fix: Iterate a curated allowlist (PresentationEmitKeys) mirroring the setter's accepted bare keys, plus a special `direction → rtl` rewrite because Get emits `direction=rtl` while the setter case key is `rtl`. Empty strings are dropped (the standard "value matches OOXML default, PopulatePresentationSettings omitted it" signal). New behavior: presentation attribute set survives round-trip. Defaults still produce zero items so unchanged decks don't gain spurious rows.
Bug: a slide with [group at zorder=1, shape at zorder=2] (group behind, shape in front) replayed as [shape, group] = [1, 2] — z-order flipped because the group's add row carried no zorder prop and AddGroup defaults to append. Root cause: EmitGroup ran FilterEmittableProps on the direct-Get of the group path, which strips zorder. The slide-enumeration NodeBuilder branch that surfaces zorder fires only when the group appears as a *child* of the slide, not on a direct Get of the group's own path. Fix: after FilterEmittableProps, look up zorder on the source grpNode (passed in by the slide walker) and preserve it. Schema follows suit — pptx/group.json declares zorder add/set/get=true (was all false), matching the AddGroup / SetGroup handler behavior that already accepted the prop.
Schema-emit drift: handler-side add/set already accepted these keys, but the help schema either omitted them entirely or marked them add=false. dump emit (which mirrors what add/set accept) produced JSON whose property set was not reflected in `officecli help <type>` output — agents inspecting the schema as the canonical contract had no way to learn the key existed. Aligned declarations: _shared/chart: dispBlanksAs, varyColors flipped to add=true (set/get were already true; emit went into the chart add row, schema lagged). pptx/chart, pptx/table: declare zorder add/set/get=true (handler accepted via ApplyShapePropsCore; emit started carrying it after the R6 shape schema alignment). pptx/paragraph: declare bold, italic, color, size, lang. dump's single-run fold path collapses a paragraph's only run's character props onto the paragraph (defRPr); the schema needs to declare them so the resulting set row's keys validate. docx/comment: declare runStart add=true. dump emits this when the comment range starts inside a paragraph (after the Nth run) so replay can restore the intra-paragraph anchor. docx/table-cell: declare skipGridSync set=true. The R3 set-side fix recognized it; the schema follows. No handler changes — pure schema declarations matching long-standing handler behavior.
…hor crossBetween before majorUnit Radar and bubble dump→batch replays produced files PowerPoint refused to open. Two schema-order bugs in chart setters: 1. The varyColors setter anchored the new element after Grouping / BarGrouping / BarDirection only. Radar and scatter chart types have no Grouping element; their schema prefix is `radarStyle, varyColors, ser*` / `scatterStyle, varyColors, ser*`. The fallback PrependChild landed varyColors before radarStyle / scatterStyle, which the OOXML validator rejects as an unexpected child. 2. The axis crossBetween setter used AppendChild on the value axis. The CT_ValAx tail is `crossAx, crosses?, crossesAt?, crossBetween?, majorUnit?, minorUnit?, dispUnits?, extLst?`, so AppendChild lands crossBetween after majorUnit when the chart builder already emitted majorUnit. PowerPoint silently rejected the resulting file. Extend the varyColors anchor chain to also accept RadarStyle / ScatterStyle, and re-anchor crossBetween after crossesAt / crosses / crossAx so it precedes majorUnit regardless of emit order.
ChartHelper.Reader emitted Format[\"transparency\"] in raw OOXML alpha units (e.g. 70000 for 30% opaque). The series transparency setter expects 0..100 percent and converts to alpha = (100 - transparency) * 1000. Replaying a dumped chart fed 70000 to the setter, producing a negative alpha element that fails the SrgbClr/Alpha MinInclusive=0 schema constraint and breaks PowerPoint open. Emit transparency as a 0..100 percent computed from (100000 - alpha) / 1000, matching the setter input contract. The companion `alpha` field still mirrors the raw OOXML units for round-trippable color-with-alpha inputs.
…eeds no run AddPlaceholder seeds the txBody first paragraph with <a:endParaRPr> only — no <a:r> element. The batch emitter walked the source paragraph and treated the seeded paragraph the same as a shape/textbox seed (which DOES include one empty <a:r>), so the first run was emitted as `set .../paragraph[1]/run[1]`. When the source paragraph carried run-only attrs (e.g. lang) without text on the paragraph-level collapse, replay targeted a non-existent run and the batch step failed. Thread a `seededFirstParaHasRun` flag through EmitTextBody / EmitParagraph and set it false for the placeholder caller. When the seeded paragraph has no run, both the multi-run path and the single-run-collapse runOnly follow-up switch from `set .../run[1]` to `add run` so the run is materialized on demand. Shape/textbox callers keep the existing rewrite-the-seed behavior to avoid the +1 phantom-run drift on round trips.
EmitRun and EmitFirstRunAsSet routed run-internal `link=slide[N]` through DummyCtxStripSlideJump, which silently removed the link prop on the assumption the shape-level emit had already deferred it. But a shape can carry both a shape-level link and per-run slide jumps on its text, and the shape-level defer only owns the shape's hyperlink. The run's slide-jump was lost on every dump, leaving the replayed run with no hyperlink at all. Thread SlideEmitContext through EmitTextBody → EmitParagraph → EmitRun/ EmitFirstRunAsSet and add DeferRunSlideJumpLink — the run analogue of DeferSlideJumpLink. Slide-jump links get queued onto ctx.DeferredLinks with the run's positional path as the target, joining the existing end-of-batch flush so the cross-slide reference resolves once every target slide is materialized. External URLs and named actions stay on the inline `add run` path (AddRun.ApplyRunHyperlink writes them directly).
…series set ChartHelper.Reader emits per-series outline as three Format keys (outlineColor, lineWidth, lineDash) but the series setter only honored the compound `outline=color:width:dash` form. Dump→batch replays of charts with explicit per-series outline lost the line spec because the emitter forwarded the read-side keys verbatim and the setter routed them to unsupported. Add `outlinecolor` / `linecolor` cases that update only the SolidFill inside the outline element (preserving any existing width / dash), and add `outlinewidth` / `outlinedash` aliases mirroring the existing `lineWidth` / `lineDash` handlers. SolidFill is inserted before any PrstDash child to keep CT_LineProperties schema order.
… on chart add
The chart batch emitter flattens NodeBuilder's per-series Format keys
onto the chart-level `add` row as dotted `series{N}.<key>` so the chart
setter can apply them after the chart is built. The flatten list
covered color / lineWidth / lineDash / marker / markerSize / smooth /
outlineColor / transparency but omitted gradient. Charts whose series
each carried a distinct GradientFill replayed against the chart-level
gradient fallback only — every series got the first series's gradient
spec.
Add gradient, outlineWidth and outlineDash to the flatten list so each
series's own spec round-trips. The dotted setter route through the
existing per-series cases (gradient / outlinewidth / outlinedash /
linecolor) already lands them correctly.
AddPlaceholder always assigned an idx to non-title placeholders by matching the layout slot. When source XML carried <p:ph type='subTitle'/> with no idx attribute, NodeBuilder correctly omitted phIndex from the dump (its emit-only-when-Index-has-value contract), but AddPlaceholder on replay auto-bound idx=1 from the layout match. The resulting <p:ph type='subTitle' idx='1'/> inherited body's default bullet style from the layout/master cascade and rendered a phantom bullet that the source never had. Detect whether the caller explicitly provided idx and, when phType is subTitle without an explicit idx, bind to the layout slot by type alone — leaving Index unset on the <p:ph> emit. ECMA-376 lets the type attribute drive the slot match for the title family; subtitle has the same property. Other placeholder types keep the existing auto-allocate behavior because they routinely share a phType across a slide (body slots at idx=1, idx=2, ...) and need the disambiguator.
…ning ids add-part smartart injects a stub <p:graphicFrame> into the slide's spTree so that GetSmartArtsOnSlide can find the new SmartArt on the next dump. The id-allocation pass scanned descendants of type DocumentFormat.OpenXml.Drawing.NonVisualDrawingProperties, but spTree's cNvPr elements live in the Presentation namespace (<p:nvSpPr>/<p:nvGrpSpPr>/<p:nvGraphicFramePr> > cNvPr) and map to DocumentFormat.OpenXml.Presentation.NonVisualDrawingProperties. The wrong SDK type matched zero descendants, leaving nextId pinned at 1 and colliding with the slide's nvGrpSpPr cNvPr id=1. After: nextId = max(existing pptx cNvPr ids) + 1, so the stub frame never reuses an id already claimed by the spTree root or any sibling.
Setting an axis title style with the compound form `title.font=14:4472C4:Arial` stored the entire literal as a LatinFont typeface, producing a chart whose axis title rendered with a font named "14:4472C4:Arial" — silently invalid in PowerPoint. The case "font" branch in the axis-title styling fan-out treated the value as a bare typeface and assigned it to LatinFont/EastAsianFont verbatim, with no parsing for the `:`-delimited triplet that the axisfont knob already supports. Fix: when the title.font value contains `:`, route through BuildDefaultRunPropertiesFromCompoundSpec (the same parser used for axisfont) and fan the parsed size/color/typeface across each run's RunProperties plus the title's DefaultRunProperties. Bare-font form (`title.font=Arial`) keeps the original single-typeface path.
Slide-level <p:clrMapOvr> and <p:extLst> children of <p:sld> were silently dropped on dump. The semantic emit path reads <p:transition> as a slide prop and uses the animation index for <p:timing>, but never touches the other two children — a non-default color-mapping override (theme accent remap on a single slide) or a slide-level extension (custom uri payloads from PowerPoint plug-ins, section IDs) vanished on dump->replay. Fix: extend ScanSlideExoticContent to capture both elements verbatim from the raw slide XML and emit a raw-set append on /p:sld for each. EnforceKnownSchemaOrder in RawXmlHelper already sorts the resulting children into spec order (cSld -> clrMapOvr -> transition -> timing -> extLst), so the append sequence remains schema-valid regardless of whether the semantic emit also injected a transition or timing. Plain (non-exotic) <p:timing> stays owned by the animation index path; raw-emitting it here would duplicate effects already produced by the per-shape add-animation rows. Verified: a slide carrying both elements round-trips byte-faithful through dump+batch with 0 validation errors.
Slides carrying audio or video shapes store volume / loop / autoPlay / trim under <p:timing>/<p:tnLst> via <p:audio><p:cMediaNode vol=… repeatCount=…> and a play-cmd <p:seq>. BuildSlideAnimationIndex models only shape entrance/exit/emphasis presets, so the semantic emit path ignored every <p:audio>/<p:video> timing node and the whole timing slice was dropped (it failed the existing exotic test, which fired only on presetClass="path" or <p:animMotion>). Net effect: after dump → batch replay, every audio/video shape lost its playback knobs (volume reset to 50, loop reset to false, autoPlay reset to false, trim cleared) even though the shape itself round-tripped. Extend the exotic-timing trigger to also include <p:audio> and <p:video> so the whole <p:timing> slice routes through the existing raw-set passthrough used for motion-path animations. Same caveat applies as for motion paths: a slide carrying both materialised animations AND a media timing block will double-emit the animation portion. In practice audio/video decks rarely also carry shape entrance/exit/emphasis effects, and the alternative (surgically slicing <p:audio>/<p:video> out of the timing tree and stitching around the animation index) is materially more work than the warning-vs-correctness tradeoff justifies.
Dumping a 3D-model deck produced byte-different output across two back-to-back dumps. The first dump emitted the morph-transition mc:AlternateContent slice with xmlns:p159 only (the prefix used by the wrapped <p159:morph>). After batch replay the SDK serialized the slide such that EVERY top-level child of <p:sld> inherited the slide root's namespace decls (xmlns:am3d, xmlns:a16, etc) verbatim — the SDK propagates root-level decls onto siblings when it cannot prove they are unused. The second dump then extracted the same slice carrying xmlns:am3d that the first dump did not. Fix: after the lift-extension-decls step in NormalizeSlideRawSlice, walk the slice root's xmlns declarations and remove any non-ambient prefix that is not referenced anywhere inside the slice — as an element-name prefix, an attribute-name prefix, or a token inside mc:Choice/@requires or @mc:Ignorable. Ambient (p/a/r/mc) decls are already governed by the existing strip pass. Verified on examples/ppt/3d-model.pptx: dump1 and dump2 are byte- equal (sha256 match), validate passes 0 errors.
…t loss
WordBatchEmitter's run-emit list filter dropped every paragraph child
whose Type was not in the {run, picture, field, ptab, break, equation,
tab, bookmark} allowlist — ole runs (typed "ole" by CreateOleNode in
WordHandler.ImageHelpers.cs) fell through silently. Dumping a paragraph
that hosted an embedded object emitted the host paragraph alone, with
no `add ole` row and no envelope.warnings entry. Callers replaying the
batch produced an OLE-stripped document and had no signal that anything
went missing.
Full OLE round-trip needs a base64-inlined carrier for the embedded
payload (Excel / Word / PowerPoint / Package binaries) plus the VML
icon image plus VML shape geometry plus ProgID + DrawAspect + the
host part-rel — the Add side currently only accepts an external
`--prop src=<file>` path. That is a backlog item; until it lands, the
right user-facing behavior is the same model pptx uses for content
that cannot round-trip: keep the surrounding structure intact and
flag the missing element in envelope.warnings so callers can decide
whether to bail.
Wiring mirrors the pptx UnsupportedWarning channel exactly:
- WordBatchEmitter gains a DocxUnsupportedWarning record + a
Warnings list on BodyEmitContext, plumbed through a new
EmitWordWithWarnings entry point (the old EmitWord overloads stay
for the existing test corpus and forward to the new path).
- A TryEmitOleRun helper recognises Type=="ole" and appends an
unsupported-element warning (with progId for grep-ability) instead
of falling through. The run-list filter is extended to accept ole
so the helper actually receives the node.
- CommandBuilder.Dump and ResidentServer's dump path forward the
warnings into envelope.warnings + a `warning: skipped ole at <path>`
stderr line; ResidentServer.BuildWarnings already maps that line
prefix to code=unsupported_element so resident and non-resident
envelopes match.
…ly fields
Bug 1 (nested field flattening): CollapseFieldChains walked from
fldChar(begin) to the FIRST fldChar(end), without tracking nesting depth.
An inner field's instrText concatenated into the outer field's instruction
string — `{IF { DATE \@ "yyyy" } > "2024" "Future" "Past"}` collapsed to
instr=`IF DATE \@ "yyyy"` with the comparison operator, false branch, and
inner field structure all gone. The "Future" display run leaked out as a
standalone run because the outer fldChar(end) was matched by the inner
field's end marker.
Bug 2 (malformed field silent loss): a field with fldChar(begin) but no
matching end fell to "pass through original node", which the
EmitParagraph run-list filter then silently dropped — fldChar is not in
the allowlist of typed rows. instrText followed it into the void.
Fix: track depth in CollapseFieldChains.
- Only the outermost instrText/separate/display contribute to this
field's collapse target.
- depth=0 marks the matching outer end; partial nested chains keep
counting.
- sawNestedField flag triggers a warning-and-passthrough emit path in
TryEmitFieldRun: AddField cannot reconstruct nested fldChar trees,
so warn the caller and preserve the cached display text rather than
silently corrupt the instruction.
- end<0 produces a synthetic field node with
_unmatchedFieldBegin=true; TryEmitFieldRun surfaces the partial
instruction as a warning, also preserving the cached display.
New behavior: nested-field documents and malformed-field documents now
yield envelope.warnings entries instead of silent data loss, and the
visible text rendered by the field is preserved in the replay output.
… stdDev) + handler hardening - chart: stop downcasing stdDev/stdErr in errBars readback (OOXML canonical is camelCase) - pptx: reject standalone tooltip set on picture without existing hyperlink (parity with shape) - pptx: refuse set on schema set:false core props (created/modified/extended.application) - word: tblGridChange capture under track-changes so colWidths-style grid revisions survive accept/reject - docx schema: add halign alias on paragraph alignment; flip _shared/equation formula to get:true (handler already emits) - word/Navigation/Query: numLevel → ilvl readback canonical key - RawSet null-arg guards on Word/Excel handlers
- pptx/chart.json chartType: inline the enum values list (the per-format override replaced the shared base atomically; without inlining `values`, programmatic consumers like SchemaContractTests lose the legal-value array).
- pptx/textbox.json + shape.json size/bold/italic/color: flip get=false; these forward to the first run on Add/Set but Get exposes only effective.size / effective.color / inheritance summary at the shape level, with bare bold/italic only on the inner run.
- _shared/table-row.json cols: revise description ("asserts the new row's cell count matches the table grid") and example to cols=2 — handler enforces match, "override" was misleading.
- pptx/table-cell.json text: override to add=false; pptx tables are strictly rectangular per OOXML, so a standalone Add cell only succeeds when the row currently has fewer cells than the grid (an illegal state). Set + Get unchanged.
tooltip is the screen-tip on a hyperlink; without a 'link' to attach to, the value would be silently dropped. Mirror the Set-side guard so callers see a descriptive ArgumentException instead of a no-op, and bundle link+tooltip in a single call (or Add the shape first, then Set link+tooltip together). CONSISTENCY(shape-tooltip-requires-link).
CheckInBackground returned early on Directory.CreateDirectory failure
and SaveConfig silently swallowed write errors. Together they meant
every officecli call in a read-only-home container respawned the
refresh process — one HEAD /releases/latest per command instead of
one per 24h, polluting origin analytics and the caller's egress.
LoadConfig + SaveConfig now iterate a candidate list: ~/.officecli/
config.json first, then $TMPDIR/officecli-config.json only when
IsInContainer() trips (docker, k8s, podman, lambda, cloudrun, gcp-
functions). Desktop and VM users never touch /tmp; iteration stops
at first success. SaveConfig returns false when every candidate
fails and CheckInBackground uses that to skip the spawn.
While here, shorten the UA: OfficeCLI/{ver} replaces OfficeCLI-
UpdateChecker/{ver}, with " (container)" appended in container envs.
Server-side stats on d.officecli.ai accept both forms via
OfficeCLI(?:-UpdateChecker)?/(\d+\.\d+\.\d+) during the rollover.
ConfigDir + ConfigPath are now get-only properties so tests can
swap \$HOME between cases without restarting the process.
… together; map friendly lineDash aliases to sys*/lg* variants
crosses / crossesAt branches mutually wiped each other: both branches called RemoveAllChildren on the COUSIN type before writing, so a single Set call that supplied both keys silently dropped whichever ran first. Restrict each branch to remove only its own type (CT_ValAx schema lets both children coexist, and a Set with both keys is the natural way to switch a value axis crossing point).
Also fix lineDash mapping per schemas/help/_shared/chart-series.json contract: friendly aliases ("dash" / "dot" / "dashDot") all resolve to the sys* OOXML variants (SystemDash / SystemDot / SystemDashDot). The previous mapping wrote the literal Dash / Dot / DashDot enum members, so Set("dash") + Get round-tripped as "dash" instead of the documented "sysDash". 'solid' remains the only round-trip-stable token.
Set bold.cs=false (and italic.cs=false) writes <w:bCs val="false"/> via the explicit-false-override path. The I18n reader only checked element presence, so Get reported bold.cs=true even after the user had toggled it off — diverging from how the bare bold / italic readback (which uses IsToggleOn) treats the same Val=false sentinel. Gate the read on element AND (Val absent OR Val truthy), so the explicit-off form rounds-trips as "absent from Format" instead of as a phantom true.
…fusals Two production hardening hunks were added in cee0b5b and parts of 84e18bb to satisfy tests that pinned aspirational "should throw" contracts; no real user reported the silent-drop / silent-no-op as a problem, so adding throws on previously-tolerant code paths is a behavior break for any existing caller passing tooltip without link, or echoing core properties through Set. Revert: - PowerPointHandler.Add.Shape.cs: shape Add no longer throws on tooltip without link (full revert of cee0b5b). - PowerPointHandler.Set.Media.cs: picture Set tooltip alone returns to "silent passthrough" (the test that wanted a throw was the only voice for the new behavior). - PowerPointHandler.Set.cs: drop the created / modified / extended.application read-only guards; Set silently ignores them as before. The substantive bug fixes from the same commits (errBars canonical case, axis crosses + crossesAt mutual-wipe, bold.cs Val=false readback, lineDash friendly aliases, schema/handler-reality alignment, tblGridChange capture, numLevel → ilvl rename) stay.
…rts to get=false Earlier session change flipped formula.get on the shared equation base from false to true on the strength of "handler already emits Format[\"formula\"]". That holds for pptx (PowerPointHandler.NodeBuilder.cs reconstructs LaTeX from <m:oMath>) but NOT for docx — WordHandler's equation Get path doesn't surface a formula key. The shared base therefore overstated docx's contract. Restore the conservative shared baseline (get=false) and add a pptx-specific override that re-asserts get=true. docx equation Get readback stays honest until WordHandler grows the matching emit (no current user has reported needing it).
The xpath / action null guards added in 84e18bb weren't fixing any reported issue — they were "while I'm here" defensive code added alongside legitimate handler-canonical fixes. No caller passes null today, and if one ever did, the NullReferenceException a few lines down is just as loud as the ArgumentNullException would be. Strip the gratuitous additions; the partPath guard (which predates this session) stays.
…ses column alignment
\mathbb{R}/\mathcal{L} emitted m:scr without required val attribute, and
in wrong child order (m:sty before m:scr violates CT_MRPR sequence). Set
val to DoubleStruck/Script and put scr before sty.
cases environment emitted m:mcJc directly under m:mc, but schema requires
it wrapped in m:mcPr. Files with matrices in cases failed validation and
would not open in Word.
…row commands Tokenizer mapped \| to literal '|'; now tokenizes as Vert command so it renders as ‖ (double vertical bar). The original behavior collapsed norm expressions like \|x\|_2 to |x|_2. Command table additions: to gets mapsto iff implies impliedby land wedge lor vee lnot neg mid parallel — all previously fell through as literal text in math output.
…urface Adds 25 examples (31-55) covering matrices (pmatrix/bmatrix/vmatrix), cases, auto-sized delimiters (left/right with various bracket types, floor/ceiling), overbrace/underbrace/overset, math fonts (mathbb/cal/ bf/rm), cancel/cancelto/boxed, accents (bar/vec/tilde/ddot/overline), hyperbolic and inverse trig, operatorname, modular arithmetic, double integral with text, big operators (bigcup/bigcap/coprod), full uppercase Greek set, matrix dots (cdots/vdots/ddots), spacing controls, textcolor, set theory, norm and inner product.
…preview Add visual indicators for revision types that were previously invisible in the HTML preview (view html / watch): - rPrChange: yellow highlight with bottom border on affected runs - pPrChange: yellow highlight with left border on affected paragraphs - tblPrChange: yellow highlight with left border on affected tables - Table row ins/del/moveFrom/moveTo: flatten revision-wrapped rows and apply green (ins) / red strikethrough (del) row styling - Add CSS classes: .track-format, .track-ins-row, .track-del-row Before this change, only w:ins and w:del run-level revisions were visible. Format changes and table row revisions were silently absent from the preview.
Add three new HTTP endpoints to the watch server: - POST /api/revision/accept — accept all tracked changes - POST /api/revision/reject — reject all tracked changes - GET /api/revision/count — return revision count as JSON All endpoints spawn officecli commands (following the existing /api/edit pattern) and notify SSE clients with a "full" refresh after accept/reject operations. Additionally, inject a floating revision toolbar into watch HTML output for Word documents: - Shows revision count badge (fetched from /api/revision/count) - Accept All (green) / Reject All (red) buttons - Auto-hides when no revisions exist - Only appears for Word documents (detected by data-block markers) - Refreshes automatically via SSE update events This enables AionUI and other watch consumers to provide revision management without any frontend code changes.
- acceptallchanges=all → revision.action=accept - rejectallchanges=all → revision.action=reject - path / → /revision - update doc comments
- Add AionuiInstaller.cs: one-shot integration that installs officecli-track-changes skill and registers Word 修订助手 assistant - Add skills/officecli-track-changes/SKILL.md: skill file defining OOXML Track Changes capabilities for AionUI agents - Add SkillInstaller entry for track-changes skill - Add early dispatch for setup-aionui command in Program.cs (placed between mcp and install blocks for early exit) - Supports --dry-run, --verbose, and --force flags - Auto-detects AionUI config directory across macOS/Linux/Windows - Handles base64url-encoded aionui-config.txt - Idempotent: detects existing skill and assistant, skips re-registration - Creates .bak backup before modifying config - Named id prefix custom-officecli-revision-* for easy identification
… fallback - Primary check: _setupBy == 'officecli' marker field (deterministic) - Fallback: name match via ToString() (avoids GetValue<string>() edge cases) - Added _setupBy marker to all new assistant entries
- Show visual preview after revisions when client supports web pages - AionUI: no action (watch server already shows live preview) - CLI-only: fall back to text summary via query revision
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概述
新增
officecli setup-aionui命令,一键将 OfficeCLI 的 Track Changes 功能集成到 AionUI 桌面应用中。功能
officecli-track-changes/SKILL.md,定义 OOXML 修订能力custom-officecli-revision-*),预配置officecli-docx+officecli-track-changes技能aionui-config.txt前创建.bak备份--dry-run、--verbose、--force文件变更
src/officecli/Core/AionuiInstaller.cssrc/officecli/Program.cssetup-aionui在 mcp / install 之前执行src/officecli/Core/SkillInstaller.cstrack-changes条目skills/officecli-track-changes/SKILL.md测试结果
构建
端到端测试(macOS arm64)
验证清单
assistants数组,无重复custom-officecli-revision-、enabledSkills包含两个技能、isPreset=true、presetAgentType=opencode.bak--force覆盖现有助手条目(新 ID),无重复--dry-run预览模式正常工作助手配置示例
{ "id": "custom-officecli-revision-1779860156330", "name": "Word 修订助手", "description": "专注 Word 文档修订(Track Changes)的助手,支持查找替换修订、格式修订、接受/拒绝修订。", "avatar": "📝", "isPreset": true, "isBuiltin": false, "presetAgentType": "opencode", "enabled": true, "enabledSkills": ["officecli-docx", "officecli-track-changes"], "customSkillNames": [] }